package opt;

import evo.TechInit;
import gui.Console;

import ids.PlacementMove;
import ids.PlacementState;

import java.util.Random;

public class SimAnneal {

	private static float initialTemperature = 20;

	private Random rnd;

	private long seed;

	private int temperatureCycles;

	private int maxTemperatureCycles;

	private int maxIdleTemperatureCycles;

	private float coolingRate;

	private float temperature;

	private int loopsAtEachTemperature;

	private int idleTemperatureCycles;

	private float constant;

	private float threshold;
	// threshold is the fraction accepted for start temperature

	private float bestFitnessSoFar;

	private float secondToTheBest;

	private PlacementState bestStateSoFar;

	private PlacementState currentState;

	private float currentFitness;

	private float nextFitness;

	private TechInit tob;

	private Console console;

	public SimAnneal() {
		this.console = new Console();
	}

	public void initialise(TechInit tob) {
		this.tob = tob;
		this.constant = (float) 1.0;
		this.threshold = (float) 0.6;
		this.idleTemperatureCycles = 0; // fruitless outer loops

		this.coolingRate = tob.getCoolingRate();
		this.seed = tob.getSeed();
		this.maxTemperatureCycles = tob.getMaxTemperatureCycles();
		this.loopsAtEachTemperature = tob.getMovesAtEachTemperature();
		this.maxIdleTemperatureCycles = tob.getMaxIdleTemperatureCycles();

		this.rnd = new Random(this.seed);

		this.temperatureCycles = 0; // current number of outer loops
		this.temperature = (float) 0.0; // current temperature
		this.currentFitness = 0;
		this.nextFitness = 0;

		this.currentState = tob.getState().copy();

		this.currentState.evalStateFitness();
		this.bestFitnessSoFar = this.currentState.getFitness();
		this.bestStateSoFar = this.currentState.copy();

		this.secondToTheBest = 0;
	}

	public void doSimulatedAnnealing() {
		// select Initial Solution done already
		this.console.println();
		this.console.println("At start of doing simulated annealing");

		this.findStartTemp();

		this.console.println("Start cooling down");
		this.reset();
		int acceptsInInnerLoop = 0;
		while (!finished()) {
			if ((temperatureCycles != 0) && (temperatureCycles % 50 == 0)) {
				this.console.println("At temperature cycle "
						+ temperatureCycles
						+ ", current temperature approximates "
						+ Math.round(this.temperature));
				this.console.println("The best score so far is "
						+ bestFitnessSoFar);
			}
			acceptsInInnerLoop = doInnerLoop(true);
			if (acceptsInInnerLoop > 0) {
				idleTemperatureCycles = 0;
			} else {
				idleTemperatureCycles++;
			}
			temperatureCycles++;
			temperature = temperature * coolingRate;
		}
	}

	private int doInnerLoop(boolean started) {
		PlacementMove m;
		int accepts = 0;
		currentState.setTemperatureCycles(this.temperatureCycles);
		currentState.setTemperature(this.temperature);
		for (int i = 0; i < loopsAtEachTemperature; i++) {
			m = currentState.getNextMoveAndFitness();
			currentState.setInnerLoops(i);
			if (this.acceptMove(m)) {
				accepts++;
				currentState.acceptMove(m);
				currentFitness = m.getFitness();
				if (currentFitness > bestFitnessSoFar) {
					bestFitnessSoFar = currentFitness;
					bestStateSoFar = currentState.copy();
				}
			}
			if ((started) && (this.bestFitnessSoFar > this.secondToTheBest)) {
				this.console.print("Find a better placement (score "
						+ bestFitnessSoFar + ")");
				this.console.println(" at temperature cycle "
						+ this.temperatureCycles + ", inner move " + i);
				this.secondToTheBest = this.bestFitnessSoFar;
			}
		}
		return accepts;
	}

	private boolean acceptMove(PlacementMove m) {
		this.nextFitness = m.getFitness();
		if (this.nextFitness > this.currentFitness) {
			return true;
		}
		float delta = this.currentFitness - this.nextFitness;
		float exponent = ((-delta) / (this.temperature * this.constant));
		return (Math.exp(exponent) > this.rnd.nextFloat());
	}

	private boolean finished() {
		return ((this.idleTemperatureCycles > this.maxIdleTemperatureCycles) | (this.temperatureCycles > this.maxTemperatureCycles));
	}

	private void findStartTemp() {
		this.temperature = initialTemperature;
		this.console
				.println("To find initial temperature: current temperature is "
						+ Math.round(this.temperature));

		int locAccepts = doInnerLoop(false);
		while (((float) locAccepts) / ((float) loopsAtEachTemperature) < threshold) {
			this.temperature *= 2.0;
			this.console.println("Reset initial temperature to "
					+ Math.round(this.temperature));
			locAccepts = doInnerLoop(false);
		}
	}

	private void reset() {
		this.temperatureCycles = 0; // current number of outer loops
		this.currentFitness = 0;
		this.nextFitness = 0;

		this.currentState = this.tob.getState().copy();

		this.currentState.setTemperature(this.temperature);
		this.currentState.evalStateFitness();
		this.bestFitnessSoFar = this.currentState.getFitness();
		this.bestStateSoFar = this.currentState.copy();

		this.secondToTheBest = 0;
	}

	public void setConsole(Console console) {
		this.console = console;
	}

	public PlacementState getBestStateSoFar() {
		return this.bestStateSoFar;
	}

	public PlacementState getCurrentState() {
		return this.currentState;
	}

	public int getIteration() {
		return this.temperatureCycles;
	}

	public long getSeed() {
		return this.seed;
	}

	public void setSeed(long seed) {
		this.seed = seed;
	}

	public int getMaxTemperatureCycles() {
		return this.maxTemperatureCycles;
	}

	public void setMaxTemperatureCycles(int maxTemperatureCycles) {
		this.maxTemperatureCycles = maxTemperatureCycles;
	}

	public int getMaxIdleTemperatureCycles() {
		return this.maxIdleTemperatureCycles;
	}

	public void setMaxIdleTemperatureCycles(int maxIdleTemperatureCycles) {
		this.maxIdleTemperatureCycles = maxIdleTemperatureCycles;
	}

	public float getCoolingRate() {
		return this.coolingRate;
	}

	public void setCoolingRate(float coolingRate) {
		this.coolingRate = coolingRate;
	}

	public int getLoopsAtEachTemperature() {
		return this.loopsAtEachTemperature;
	}

	public void setLoopsAtEachTemperature(int loopsAtEachTemperature) {
		this.loopsAtEachTemperature = loopsAtEachTemperature;
	}

}